All files / Bandstand/src/app/api/proposals/[id]/vote route.ts

100% Statements 70/70
85.71% Branches 12/14
100% Functions 2/2
100% Lines 70/70

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 831x   1x 1x 1x   1x 1x   2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 2x   5x 5x 5x 5x 5x 5x 5x   1x 1x 4x 4x 4x 4x   3x 3x 2x 2x 2x 2x 2x 3x 4x   1x 1x   2x 2x 4x 1x   1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x   1x   1x 1x 1x 1x  
export const dynamic = 'force-dynamic'
 
import {prisma} from '@/lib/db'
import {requireSession} from '@/lib/guard'
import {bus, EVENTS} from '@/lib/events'
import {Prisma} from '@prisma/client'
import {addSongFromProposal} from '@/lib/songs'
import {route} from '@/lib/route'
 
async function withPromotion(
  tx: Prisma.TransactionClient,
  proposalId: string,
  userId: string,
  band: {id: string; voteThreshold: number; tributeTo: string | null},
) {
  const threshold = band.voteThreshold
  const voteCount = await tx.vote.count({where: {proposalId}})
  const p = await tx.proposal.findUnique({where: {id: proposalId}})
  if (!p) return
  if (p.status === 'PENDING' && voteCount >= threshold) {
    await tx.proposal.update({
      where: {id: proposalId},
      data: {status: 'APPROVED'},
    })
    await addSongFromProposal(tx, p, userId, band)
  }
}
 
async function inBand(proposalId: string, bandId: string) {
  const p = await prisma.proposal.findFirst({
    where: {id: proposalId, bandId},
    select: {id: true},
  })
  return Boolean(p)
}
 
export const POST = route(
  async (_req: Request, {params}: {params: {id: string}}) => {
    const {user, band} = await requireSession()
    const pid = params.id
    if (!(await inBand(pid, band.id)))
      return new Response('Not Found', {status: 404})
 
    try {
      await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
        await tx.vote.create({data: {userId: user.id, proposalId: pid}})
        await tx.auditLog.create({
          data: {userId: user.id, action: 'VOTE', targetId: pid},
        })
        await withPromotion(tx, pid, user.id, band)
      })
    } catch {
      // unique(userId, proposalId) constraint trip -> conflict
      return new Response('Conflict', {status: 409})
    }
 
    bus.emit(EVENTS.PROPOSAL_UPDATED, {id: pid, bandId: band.id})
    return new Response(null, {status: 204})
  },
)
 
export const DELETE = route(
  async (_req: Request, {params}: {params: {id: string}}) => {
    const {user, band} = await requireSession()
    const pid = params.id
    if (!(await inBand(pid, band.id)))
      return new Response('Not Found', {status: 404})
 
    await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
      await tx.vote.delete({
        where: {userId_proposalId: {userId: user.id, proposalId: pid}},
      })
      await tx.auditLog.create({
        data: {userId: user.id, action: 'UNVOTE', targetId: pid},
      })
      // No auto-demote in v1
    })
 
    bus.emit(EVENTS.PROPOSAL_UPDATED, {id: pid, bandId: band.id})
    return new Response(null, {status: 204})
  },
)